package com.googlecode.cswish.struts.spring;

import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.struts2.util.ClassLoaderUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.TypeConverter;
import org.springframework.beans.factory.BeanDefinitionStoreException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.MessageSourceResolvable;
import org.springframework.context.NoSuchMessageException;
import org.springframework.core.io.Resource;

public class AutoLoadApplicationContext implements ApplicationContext
	, AutowireCapableBeanFactory, BeanDefinitionRegistry {

	private static final Log log = LogFactory.getLog(AutoLoadApplicationContext.class);
	
	private ApplicationContext parent;
	
	private AutowireCapableBeanFactory autowire;
	
	private BeanDefinitionRegistry register;
	
	private final Map<String, String> registedBeans = new HashMap<String, String>();
	
	public AutoLoadApplicationContext(ApplicationContext parent, AutowireCapableBeanFactory autowire
			, BeanDefinitionRegistry register) {
		this.parent = parent;
		this.autowire = autowire;
		this.register = register;
	}
	
	@Override
	public AutowireCapableBeanFactory getAutowireCapableBeanFactory()
			throws IllegalStateException {
		return parent.getAutowireCapableBeanFactory();
	}

	@Override
	public String getDisplayName() {
		return parent.getDisplayName();
	}

	@Override
	public String getId() {
		return parent.getId();
	}

	@Override
	public ApplicationContext getParent() {
		return parent;
	}

	@Override
	public long getStartupDate() {
		return parent.getStartupDate();
	}

	@Override
	public String[] getBeanNamesForType(Class type) {
		return parent.getBeanNamesForType(type);
	}

	@Override
	public String[] getBeanNamesForType(Class type,
			boolean includeNonSingletons, boolean allowEagerInit) {
		return parent.getBeanNamesForType(type, includeNonSingletons, allowEagerInit);
	}

	@Override
	public Map getBeansOfType(Class type) throws BeansException {
		return parent.getBeansOfType(type);
	}

	@Override
	public Map getBeansOfType(Class type, boolean includeNonSingletons,
			boolean allowEagerInit) throws BeansException {
		return parent.getBeansOfType(type, includeNonSingletons, allowEagerInit);
	}

	@Override
	public boolean containsBean(String name) {
		return parent.containsBean(name);
	}

	private boolean autoLoadBeanDefinition(String beanName, Class<?> clazz) {
		if (beanName == null && clazz == null) {
			return false;
		}
		
		BeanDefinition def = null;
		try {
			if (clazz == null) {
				clazz = ClassLoaderUtils.loadClass(beanName, this.getClass());
			}
//			GenericBeanDefinition bd = new GenericBeanDefinition();
//			bd.setBeanClass(clazz);
			// Default instance is singleton
			RootBeanDefinition bd = new RootBeanDefinition(clazz, AutowireCapableBeanFactory.AUTOWIRE_BY_NAME);
			if (beanName == null) {
				beanName = clazz.getName();
			}
			bd.setPrimary(true);
			javax.annotation.Resource resource = clazz.getAnnotation(javax.annotation.Resource.class);
			if (resource != null) {
				bd.setScope(resource.shareable() ? BeanDefinition.SCOPE_SINGLETON : BeanDefinition.SCOPE_PROTOTYPE);
				if (resource.mappedName().length() > 0) {
					beanName = resource.mappedName();
				}
			}
			
			registerBeanDefinition(beanName, bd);
			def = bd;
		} catch (Throwable ex) {
			log.debug("Can't auto-register bean defeintion:" + beanName);
		}
		return def != null;
	}
	
	@Override
	public Object getBean(String name) throws BeansException {
		final String theName = name;
		return fixNoBeanDefException(name, new Callback() {
			@Override
			public Object execute() {
				return parent.getBean(theName);
			}
		});
	}
	
	private Object fixNoBeanDefException(String lastBeanName, Callback callback) throws BeansException {
		try {
			// use a simple way to avoid the synchronized block
			if (registedBeans.get(lastBeanName) != null) {
				return callback.execute();
			}
		} catch (BeansException e) {
		}
		
		BeansException error = null;
		// Spring has a syn defect in org.springframework.beans.factory.support.DefaultListableBeanFactory
		// see code: 
		// protected void resetBeanDefinition(String beanName) {
		//     ...
		//     synchronized (getSingletonMutex()) {
		// }
		// so, we add 'synchronized' to avoid the problem
		synchronized (this) {
			boolean tryIt = true;
			while (tryIt) {
				try {
					Object bean = callback.execute();
					registedBeans.put(lastBeanName, lastBeanName);
					return bean;
				} catch (BeansException ex) {
					error = ex;
					Throwable cause = ex;
					while (cause != null && !(cause instanceof NoSuchBeanDefinitionException)) {
						cause = cause.getCause();
					}
					
					if (cause != null) {
						NoSuchBeanDefinitionException noBeanEx = (NoSuchBeanDefinitionException) cause;
						String message = noBeanEx.getMessage();
						// Exclude the case: the configuration registered many class objects
						boolean isMissedDefinition = message != null 
							&& (message.indexOf("expected single matching bean but found") == -1
									&& message.indexOf("more than one 'primary' bean found among candidates") == -1);
						if (isMissedDefinition) {	// avoid dead cycle
							// Try to load it temporarily
							tryIt = autoLoadBeanDefinition(noBeanEx.getBeanName(), noBeanEx.getBeanType());
						} else {
							tryIt = false;
						}
					} else {
						tryIt = false;
					}
				}
			}
		}
		throw error;
	}

	@Override
	public Object getBean(String name, Class requiredType)
			throws BeansException {
		return parent.getBean(name, requiredType);
	}

	@Override
	public Object getBean(String name, Object[] args) throws BeansException {
		return parent.getBean(name, args);
	}

	@Override
	public Class getType(String name) throws NoSuchBeanDefinitionException {
		return parent.getType(name);
	}

	@Override
	public boolean isPrototype(String name)
			throws NoSuchBeanDefinitionException {
		return parent.isPrototype(name);
	}

	@Override
	public boolean isSingleton(String name)
			throws NoSuchBeanDefinitionException {
		return parent.isSingleton(name);
	}

	@Override
	public boolean isTypeMatch(String name, Class targetType)
			throws NoSuchBeanDefinitionException {
		return parent.isTypeMatch(name, targetType);
	}

	@Override
	public boolean containsLocalBean(String name) {
		return parent.containsLocalBean(name);
	}

	@Override
	public BeanFactory getParentBeanFactory() {
		return parent;
	}

	@Override
	public String getMessage(MessageSourceResolvable resolvable, Locale locale)
			throws NoSuchMessageException {
		return parent.getMessage(resolvable, locale);
	}

	@Override
	public String getMessage(String code, Object[] args, Locale locale)
			throws NoSuchMessageException {
		return parent.getMessage(code, args, locale);
	}

	@Override
	public String getMessage(String code, Object[] args, String defaultMessage,
			Locale locale) {
		return parent.getMessage(code, args, defaultMessage, locale);
	}

	@Override
	public void publishEvent(ApplicationEvent event) {
		parent.publishEvent(event);
	}

	@Override
	public Resource[] getResources(String locationPattern) throws IOException {
		return parent.getResources(locationPattern);
	}

	@Override
	public ClassLoader getClassLoader() {
		return parent.getClassLoader();
	}

	@Override
	public Resource getResource(String location) {
		return parent.getResource(location);
	}
	
	@Override
	public String[] getAliases(String name) {
		return parent.getAliases(name);
	}

	@Override
	public boolean containsBeanDefinition(String beanName) {
		return parent.containsBeanDefinition(beanName);
	}

	@Override
	public int getBeanDefinitionCount() {
		return parent.getBeanDefinitionCount();
	}

	@Override
	public String[] getBeanDefinitionNames() {
		return parent.getBeanDefinitionNames();
	}
	
	///////////////////////////////////////////////////////////////////////////
	@Override
	public Object applyBeanPostProcessorsAfterInitialization(
			Object existingBean, String beanName) throws BeansException {
		return autowire.applyBeanPostProcessorsAfterInitialization(existingBean, beanName);
	}

	@Override
	public Object applyBeanPostProcessorsBeforeInitialization(
			Object existingBean, String beanName) throws BeansException {
		return autowire.applyBeanPostProcessorsBeforeInitialization(existingBean, beanName);
	}

	@Override
	public void applyBeanPropertyValues(Object existingBean, String beanName)
			throws BeansException {
		autowire.applyBeanPropertyValues(existingBean, beanName);		
	}

	@Override
	public Object autowire(Class beanClass, int autowireMode,
			boolean dependencyCheck) throws BeansException {
		return autowire.autowire(beanClass, autowireMode, dependencyCheck);
	}

	@Override
	public void autowireBean(Object existingBean) throws BeansException {
		autowire.autowireBean(existingBean);
	}

	@Override
	public void autowireBeanProperties(Object existingBean, int autowireMode,
			boolean dependencyCheck) throws BeansException {
		final Object theExistingBean = existingBean;
		final int theAutowireMode = autowireMode;
		final boolean theDependencyCheck = dependencyCheck;
		fixNoBeanDefException(null, new Callback() {
			@Override
			public Object execute() {
				autowire.autowireBeanProperties(theExistingBean, theAutowireMode, theDependencyCheck);
				return null;
			}
		});
	}

	@Override
	public Object configureBean(Object existingBean, String beanName)
			throws BeansException {
		return autowire.configureBean(existingBean, beanName);
	}

	@Override
	public Object createBean(Class beanClass) throws BeansException {
		return autowire.createBean(beanClass);
	}

	@Override
	public Object createBean(Class beanClass, int autowireMode,
			boolean dependencyCheck) throws BeansException {
		return autowire.createBean(beanClass, autowireMode, dependencyCheck);
	}

	@Override
	public Object initializeBean(Object existingBean, String beanName)
			throws BeansException {
		return autowire.initializeBean(existingBean, beanName);
	}

	@Override
	public Object resolveDependency(DependencyDescriptor descriptor,
			String beanName) throws BeansException {
		return autowire.resolveDependency(descriptor, beanName);
	}

	@Override
	public Object resolveDependency(DependencyDescriptor descriptor,
			String beanName, Set autowiredBeanNames, TypeConverter typeConverter)
			throws BeansException {
		return autowire.resolveDependency(descriptor, beanName, autowiredBeanNames, typeConverter);
	}
	
	///////////////////////////////////////////////////////////////////////////
	@Override
	public BeanDefinition getBeanDefinition(String beanName)
			throws NoSuchBeanDefinitionException {
		BeanDefinition def = null;
		if (!register.containsBeanDefinition(beanName)) {
			autoLoadBeanDefinition(beanName, null);
		}
		return register.getBeanDefinition(beanName);
	}

	@Override
	public boolean isBeanNameInUse(String beanName) {
		return register.isBeanNameInUse(beanName);
	}

	@Override
	public void registerBeanDefinition(String beanName,
			BeanDefinition beanDefinition) throws BeanDefinitionStoreException {
		synchronized (this) {
			register.registerBeanDefinition(beanName, beanDefinition);
		}
	}

	@Override
	public void removeBeanDefinition(String beanName)
			throws NoSuchBeanDefinitionException {
		register.removeBeanDefinition(beanName);
	}

	@Override
	public boolean isAlias(String beanName) {
		return register.isAlias(beanName);
	}

	@Override
	public void registerAlias(String name, String alias) {
		synchronized (this) {
			register.registerAlias(name, alias);
		}
	}

	@Override
	public void removeAlias(String alias) {
		register.removeAlias(alias);
	}
	
	interface Callback {
		Object execute();
	}
}
